namespace Specifications.Extensions
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Reflection;
    using NUnit.Framework;

    public abstract class PropertyAssertion<T> where T : class, INotifyPropertyChanged
    {
        private readonly INotifyPropertyChanged _propertyOwner;
        protected readonly IDictionary<PropertyInfo, object> setValues = new Dictionary<PropertyInfo, object>();

        protected PropertyAssertion(INotifyPropertyChanged propertyOwner)
        {
            _propertyOwner = propertyOwner;
        }

        public void Execute()
        {
            IEnumerable<PropertyInfo> candidates = GetCandidateProperties();

            AssertThatClassHasCandidateProperties(candidates);

            Dictionary<PropertyInfo, bool> results = candidates
                .ToDictionary<PropertyInfo, PropertyInfo, bool>(p => p, DoesPropertyRaiseNotification);

            var failures = (from result in results
                            where result.Value == false
                            select new {result.Key})
                .ToList();


            if (failures.Count == 0) return;

            string msg = string.Format(
                "\nThe following properties on {0} did not raise change notification:\n",
                typeof (T).Name);

            failures.ForEach(failure =>
                                 {
                                     object setValue = GetSetterValueForProperty(failure.Key);
                                     msg += string.Format("\t{0} with the value set to '{1}'\n",
                                                          failure.Key.Name,
                                                          setValue);
                                 });

            throw new AssertionException(msg);
        }

        protected abstract IEnumerable<PropertyInfo> GetCandidateProperties();

        private static void AssertThatClassHasCandidateProperties(IEnumerable<PropertyInfo> candidates)
        {
            if (candidates.Count() > 0) return;

            string msg = string.Format(
                "\n{0} does not have any public properties with setters.\n" +
                "Asserting change notification is without meaning.",
                typeof (T).Name);
            throw new AssertionException(msg);
        }

        private bool DoesPropertyRaiseNotification(PropertyInfo propertyInfo)
        {
            bool has_property_changed = false;

            _propertyOwner.PropertyChanged +=
                (s, e) => { if (e.PropertyName == propertyInfo.Name) has_property_changed = true; };

            object valueToSet = GetSetterValueForProperty(propertyInfo);
            propertyInfo.SetValue(_propertyOwner, valueToSet, null);

            return has_property_changed;
        }

        private object GetSetterValueForProperty(PropertyInfo propertyInfo)
        {
            return (setValues.ContainsKey(propertyInfo))
                       ? setValues[propertyInfo]
                       : Default(propertyInfo.PropertyType);
        }

        private static object Default(Type type)
        {
            return type.IsValueType
                       ? Activator.CreateInstance(type)
                       : null;
        }
    }
}